﻿using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace NanoMvvm
{
    public class WeakEventHandlerContainer<TSender, TArgs> : IWeakEventHandlerContainer<TSender, TArgs>, IEquatable<WeakEventHandlerContainer<TSender, TArgs>>, 
        IEquatable<Action<TSender, TArgs>>
        where TSender : class where TArgs : EventArgs
    {
        private delegate void OpenDelegateHandler<TS, TA>(object instance, TS sender, TA args);

        private readonly Action<TSender, TArgs> staticInvoker;
        private readonly OpenDelegateHandler<TSender, TArgs> instanceInvoker;
        private readonly MethodInfo instanceMethodInfo;
        private readonly WeakReference<object> target;

        private readonly bool isStatic;

        public WeakEventHandlerContainer(Action<TSender, TArgs> handler)
        {
            if (handler == null) throw new ArgumentNullException("handler");
            if (handler.Method.DeclaringType.IsValueType) throw new ArgumentException("Declaring type must be a reference type.");

            if (handler.Target == null)
            {
                Debug.Assert(handler.Method.IsStatic);

                isStatic = true;
                staticInvoker = handler;
            }
            else
            {
                Debug.Assert(!handler.Method.IsStatic);

                target = new WeakReference<object>(handler.Target);

                instanceInvoker = CreateOpenActionDelegate(handler);

                instanceMethodInfo = handler.Method;
            }
        }


        public void Invoke(TSender sender, TArgs args)
        {
            if (isStatic)
            {
                staticInvoker(sender, args);
            }
            else if (target.IsTargetAlive)
            {
                instanceInvoker(target.Target, sender, args);
            }
            else
            {
                throw new InvalidOperationException("Delegate target instance garbage collected before call.");
            }
        }

        public Action<TSender, TArgs> ToDelegate()
        {
            if(isStatic)
            {
                return staticInvoker;
            }

            if(!target.IsTargetAlive) throw new InvalidOperationException("Target garbage collected.");

            return (Action<TSender, TArgs>) Delegate.CreateDelegate(typeof (Action<TSender, TArgs>), target.Target, instanceMethodInfo);
        }

        public bool IsTargetAlive
        {
            get { return isStatic || target.IsTargetAlive; }
        }

        private static OpenDelegateHandler<TS, TA> CreateOpenActionDelegate<TS, TA>(Action<TS, TA> action)
        {
            Debug.Assert(action != null);
            Debug.Assert(!action.Method.IsStatic);

            MethodInfo method = action.Method;
            Type senderType = typeof(TS);
            Type argType = typeof(TA);

            ParameterExpression instanceExpression = Expression.Parameter(typeof(object));
            ParameterExpression senderExpression = Expression.Parameter(senderType);
            ParameterExpression argsExpression = Expression.Parameter(argType);

            Expression<OpenDelegateHandler<TS, TA>> expression = Expression.Lambda<OpenDelegateHandler<TS, TA>>(
                Expression.Call(
                    Expression.Convert(instanceExpression, action.Method.DeclaringType),
                    method,
                    senderExpression,
                    argsExpression
                ),
                instanceExpression,
                senderExpression,
                argsExpression
            );

            return expression.Compile();
        }


        public override int GetHashCode()
        {
            return isStatic ? staticInvoker.GetHashCode() : instanceInvoker.GetHashCode();
        }

        public override bool Equals(object other)
        {
            if (ReferenceEquals(null, other)) return false;

            var wehc = other as WeakEventHandlerContainer<TSender, TArgs>;
            if (!ReferenceEquals(null, wehc))
            {
                return Equals(wehc);
            }

            var a = other as Action<TSender, TArgs>;
            if (!ReferenceEquals(null, a))
            {
                return Equals(a);
            }

            return false;
        }

        public bool Equals(WeakEventHandlerContainer<TSender, TArgs> other)
        {
            if (ReferenceEquals(null, other)) return false;

            if (other.isStatic)
            {
                return isStatic && other.staticInvoker.Equals(staticInvoker);
            }

            return !isStatic && target.IsTargetAlive && //other.target.IsTargetAlive &&
                   other.instanceMethodInfo.Equals(instanceMethodInfo) &&
                   ReferenceEquals(other.target.Target, target.Target);
        }

        public bool Equals(Action<TSender, TArgs> other)
        {
            if (ReferenceEquals(null, other)) return false;

            if (other.Target == null)
            {
                Debug.Assert(other.Method.IsStatic);

                return isStatic && other.Method.Equals(staticInvoker.Method);
            }

            Debug.Assert(!other.Method.IsStatic);

            return !isStatic && target.IsTargetAlive &&
                   other.Method.Equals(instanceMethodInfo) &&
                   ReferenceEquals(other.Target, target.Target);
        }
    }
}